/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University. All rights reserved.
 * 
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license; 2) All redistributions in binary form
 * must reproduce the above copyright notice, this list of conditions and the
 * disclaimer listed in this license in the documentation and/or other materials
 * provided with the distribution; 3) Any documentation included with all
 * redistributions must include the following acknowledgement:
 * 
 * "This product includes software developed by the Community Grids Lab. For
 * further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/."
 * 
 * Alternatively, this acknowledgement may appear in the software itself, and
 * wherever such third-party acknowledgments normally appear.
 * 
 * 4) The name Indiana University or Community Grids Lab or NaradaBrokering,
 * shall not be used to endorse or promote products derived from this software
 * without prior written permission from Indiana University. For written
 * permission, please contact the Advanced Research and Technology Institute
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5) Products
 * derived from this software may not be called NaradaBrokering, nor may Indiana
 * University or Community Grids Lab or NaradaBrokering appear in their name,
 * without prior written permission of ARTI.
 * 
 * 
 * Indiana University provides no reassurances that the source code provided
 * does not infringe the patent or any other intellectual property rights of any
 * other entity. Indiana University disclaims any liability to any recipient for
 * claims brought by any other entity based on infringement of intellectual
 * property rights or otherwise.
 * 
 * LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO
 * WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 * NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
package cgl.narada.protocol;

/**
 * <b>ConnectionPropogation</b><br>
 * This class is responsible for the propagation of a connection within only the
 * relevant units of the subsystem. Thus a level-0 gateway that has been added,
 * needs to have its information disseminated only within the confines of the
 * cluster that it belongs to. In general a level <i>l</i> gateway should be
 * disseminated only within the confines of <i>(l+1) </i> unit that it belongs
 * to. Connections thus received are used to update the connectivity graph, and
 * result in the updating of the routing cache.
 * 
 * This class performs this function for connections that are added as a result
 * of the node addition protocol or gateways established using the gateway
 * propagation protocol.
 * 
 * @author Shrideep Pallickara $Revision$ $Date$
 */

public class ConnectionPropagation implements ProtocolDebugFlags {

    /** The protocol handler */
    private ProtocolHandler protocolHandler;

    /** The gatewayInfo */
    private GatewayInfo gatewayInfo;

    /** The systemLevel */
    private int systemLevel;

    /** The node's address */
    private NodeAddress thisNodeAddress;

    /** This destination */
    private Destinations thisDestination;


    /**
     * The connection Propagation constructor
     * 
     * @param gatewayInfo
     *            The connectivity graph
     * @param protocolHandler
     *            The protocol handler for the server node.
     */
    public ConnectionPropagation(GatewayInfo gatewayInfo,
            ProtocolHandler protocolHandler) {
        this.gatewayInfo = gatewayInfo;
        this.protocolHandler = protocolHandler;
        thisNodeAddress = protocolHandler.getAssignedAddress();
        thisDestination = protocolHandler.getNodeDestination();
        systemLevel = gatewayInfo.getSystemLevel();
        if (ConnectionPropagation_Debug)
            System.out
                .println("ConnectionPropagation: Initialization complete ");
    }


    /**
     * When a node/unit is added to the system. The connection that has been
     * added is propogated within the relevant context, as defined by the rules
     * pertaining to connection dissemination. This method is responsible for -
     * <li> Retrieving this connection information, and adding it to the
     * connectivity graph.
     * <li> Depending on the kind of gateway over which this hop was received,
     * we need to recreate destinations at differing levels. Thus if we receive
     * information over a level-1 gateway we need to calculate destinations at
     * level-0.
     * <li> Route this connection information to destinations where it hasn't
     * yet been routed to.
     * 
     * @param connectionPropBuffer
     *            This is the byte stream that is received over a link
     */
    public void processConnectionPropagation(byte[] connectionPropBuffer) {
        boolean connectionAdded = false;
        int offset = 0;

        if (connectionPropBuffer[offset] == Protocol.CONNECTION_ADDED) {
            connectionAdded = true;
        }
        offset++; /*
                     * Skipping the CONNECTION_ADDED header byte, already read
                     * by the protocol Handler
                     */

        /**
         * Retrieving the connection information and adding the connection
         * information into the connectivity graph viz. the gatewayInfo
         */
        byte[] connectionBytes = new byte[21];
        System.arraycopy(connectionPropBuffer, offset, connectionBytes, 0, 21);
        Connection connection =
            new Connection(thisNodeAddress, connectionBytes);

        ProtocolID _connectionId = connection.getConnectionId();

        if (connectionAdded) {
            if (gatewayInfo.containsConnection(_connectionId)) {
                /**
                 * No further processing needs to be performed for this
                 * connection addition ....
                 */
                System.out.println("ConnectionPropagation::processConnAdd()->"
                    + " This connection has been received at this node"
                    + " suppressing connection information");
                return;
            }
        } else {
            /**
             * In the case of a connection loss for processing this connection
             * should already be present in the connectivity graph. Otherwise,
             * its safe to assume that this connection information had been
             * processed earlier
             */
            if (!gatewayInfo.containsConnection(_connectionId)) {
                System.out.println("ConnectionPropagation::"
                    + " This connection removal probably been received"
                    + " a this node suppressing connection " + connection
                    + " information with connectionId" + _connectionId);
                return;
            }
        }

        if (connectionAdded) {
            gatewayInfo.addConnection(connection);
            System.out.println("ConnectionPropagation:: Added connection "
                + connection + " to the Connectivity Graph ");
        } else {
            gatewayInfo.removeConnection(connection);
            System.out.println("ConnectionPropagation:: Removed connection "
                + connection + " from the Connectivity Graph ");
        }

        offset += 21;

        /**
         * Unmarshall the traversedSoFar and toReach destinations contained in
         * the protocol packet
         */
        byte[] toReachBytes = new byte[4 * (systemLevel + 1)];
        System.arraycopy(connectionPropBuffer, offset, toReachBytes, 0,
                         4 * (systemLevel + 1));
        offset += (4 * (systemLevel + 1));

        byte[] traversedSoFarBytes = new byte[4 * (systemLevel + 1)];
        System.arraycopy(connectionPropBuffer, offset, traversedSoFarBytes, 0,
                         4 * (systemLevel + 1));
        Destinations toReach = new Destinations(toReachBytes);
        Destinations traversedSoFar = new Destinations(traversedSoFarBytes);

        int gatewayLevel = connection.getGatewayLevel();
        processReceivedDestinations(toReach, traversedSoFar, gatewayLevel);
        disseminateConnectionInformation(toReach, traversedSoFar, connection,
                                         connectionAdded);
    }


    private void processReceivedDestinations(Destinations toReach,
                                             Destinations traversedSoFar,
                                             int gatewayLevel) {
        /**
         * Depending on the kind of gateway over which this hop was received, we
         * need to recreate destinations at differing levels. Thus if we receive
         * information over a level-1 gateway we need to calculate destinations
         * at level-0
         */
        Destinations knownDest = gatewayInfo.getListOfDestinationsKnownToNode();

        for (int i = gatewayLevel; i >= 0; i--) {
            if (toReach.getDestinationsAtLevel(i) == 0) {
                toReach.setDestinationsAtLevel(i, knownDest
                    .getDestinationsAtLevel(i));
            }
        }
        /** Update traversedSoFar to include this destination */
        traversedSoFar.markAsTraversedAt(thisDestination);
        // if (ConnectionPropagation_Debug)
        System.out.println("ConnPropagn::ProcessReceivedDestinations -> "
            + "ToReach " + toReach + "Traversed So Far" + traversedSoFar);
    }


    /**
     * This method is invoked by the node which has received a node set up
     * request, after the setup is complete. This method computes the
     * destinations that need to receive this information. Besides this the
     * method also updates the routing information contained in the protocol
     * packet to reflect the destinations which have already received this
     * connection viz. the nodes comprising the gateway.
     * 
     * @param connection
     *            The connection that has been established
     */
    public void propagateConnectionInformation(Connection connection,
                                               boolean connectionAdded) {

        /**
         * Create a destination list "toReach" which includes the list that it
         * needs to reach. "traversedSoFar" which declares these two hops as
         * having been traversed already
         */
        int gatewayLevel = connection.getGatewayLevel();
        Destinations toReach = computeDestinationsToRoute(gatewayLevel);
        Destinations traversedSoFar = new Destinations(systemLevel);

        Gateway gatewayOne =
            new Gateway(gatewayInfo.getVertexNode(), connection.getNodeOne(),
                        (short) connection.getLevelOne());

        Gateway gatewayTwo =
            new Gateway(gatewayInfo.getVertexNode(), connection.getNodeTwo(),
                        (short) connection.getLevelTwo());
        traversedSoFar.updateDestinationList(gatewayOne);
        traversedSoFar.updateDestinationList(gatewayTwo);
        traversedSoFar.markAsTraversedAt(thisDestination);

        if (ConnectionPropagation_Debug)
            System.out.println("ConnPropogn:propagateConnInfo() ->"
                + "ToReach " + toReach + "TraversedSoFar " + traversedSoFar);
        disseminateConnectionInformation(toReach, traversedSoFar, connection,
                                         connectionAdded);
    }


    private Destinations computeDestinationsToRoute(int gatewayLevel) {
        Destinations toReach = new Destinations(systemLevel);
        toReach.markAsTraversedAt(thisDestination);

        Destinations knownDest = gatewayInfo.getListOfDestinationsKnownToNode();

        for (int i = gatewayLevel; i >= 0; i--) {
            toReach.setDestinationsAtLevel(i, knownDest
                .getDestinationsAtLevel(i));
        }

        return toReach;
    }


    private void disseminateConnectionInformation(Destinations toReach,
                                                  Destinations traversedSoFar,
                                                  Connection connection,
                                                  boolean connectionAdded) {
        int gatewayLevel = connection.getGatewayLevel();
        Gateway[] hopsToTake =
            gatewayInfo.hopsToReachDestination(toReach, traversedSoFar);
        /** if connection.level < hop.level ... don not send over the hop */
        if (hopsToTake != null) {
            for (int i = 0; i < hopsToTake.length; i++) {
                if (hopsToTake[i].getLevel() > gatewayLevel) {
                    /**
                     * This hop can't be used since we are not supposed to route
                     * it over a higher level hop. This scenario will not take
                     * place, and should signal an error somewhere.
                     */
                    System.out
                        .println("ConnectionPropagation: Hop returned has level "
                            + " greater than the gatewayLevel");
                    continue;
                }
                /**
                 * Depending on the kind of hop that you are gonna be sending
                 * the connection ... send the relevant connection information
                 * over the link
                 */
                byte[] byteStream =
                    prepareConnectionBytesToSendOverHop(hopsToTake[i]
                        .getLevel(), connection, connectionAdded, toReach,
                                                        traversedSoFar);
                protocolHandler.sendToNode(hopsToTake[i], byteStream);
                System.out
                    .println("Connection Propogate: Byte stream sent over hop "
                        + hopsToTake[i]);
            }
            return;
        }
        System.out.println(" Connection Propogate: - No more hops to take");
    }


    private byte[] prepareConnectionBytesToSendOverHop(
                                                       int hopLevel,
                                                       Connection connection,
                                                       boolean connectionAdded,
                                                       Destinations toReach,
                                                       Destinations traversedSoFar) {
        byte[] connPropagateBytes =
            new byte[1 /* protocol header */+ 21 /* connection bytes */+ 4
                * (systemLevel + 1) /* toReach */+ 4 * (systemLevel + 1) /* traversedSoFar */];
        int offset = 0;
        if (connectionAdded)
            connPropagateBytes[offset++] = Protocol.CONNECTION_ADDED;
        else
            connPropagateBytes[offset++] = Protocol.CONNECTION_LOST;
        /** Marshalling the connection to send over "hopLevel" gateway */
        System.arraycopy(connection.snapshotConnectionInfo(hopLevel), 0,
                         connPropagateBytes, offset, 21);
        offset += 21;

        /** Marshalling the destinations that need to be reached */
        System.arraycopy(toReach.prepareDestinationsToSendOverLevel(hopLevel),
                         0, connPropagateBytes, offset, 4 * (systemLevel + 1));
        offset += (4 * (systemLevel + 1));

        /**
         * Marshalling information regarding the nodes that have been traversed
         * so far
         */
        System.arraycopy(traversedSoFar
            .prepareDestinationsToSendOverLevel(hopLevel), 0,
                         connPropagateBytes, offset, 4 * (systemLevel + 1));
        return connPropagateBytes;
    }


    /**
     * When a new node/unit is added to the existing system, the newly added
     * unit needs to recreate the view of the connectivitives that exist
     * throughout the system. Depending on the kind of gateway this serialized
     * stream, of the graph, is sent over the information sent varies. This
     * method is responsible for processing such a buffer that has been sent
     * over a link.
     * 
     * @param connectionsBufferStream
     *            The serialized stream of relevant connections that has been
     *            received over a gateway.
     */
    public void processConnectionBuffer(byte[] connectionsBufferStream) {
        System.out
            .println("ConnectionPropagation: processing connection buffer");
        byte[] connectionsBuffer = new byte[connectionsBufferStream.length - 1];
        System.arraycopy(connectionsBufferStream, 1, connectionsBuffer, 0,
                         connectionsBufferStream.length - 1);
        gatewayInfo.processConnectionBuffer(connectionsBuffer);
    }


    /**
     * In response to a sub system (LEVEL <i>level</i>) being added to an
     * existing system, the node belonging to the system needs to exachange data
     * regarding gateways at level onwards in the newly merged system. This
     * method provides a view of the connectivity graph that would be valid at
     * the other node. This provides a serialized list of the connections that
     * need to be sent over a certain level and recreates a view of the
     * connectivitives that exist throughout the system. Depending on the kind
     * of gateway this serialized stream, of the graph, is sent over the
     * information sent varies. This method is responsible for creating such an
     * information exchange buffer and sending it over the link.
     * 
     * @param hop
     *            The gateway over which the serialized stream of relevant
     *            connections needs to be sent over.
     */
    public void propagateConnectionBufferToHop(Gateway hop) {
        byte[] connectionBuffer =
            gatewayInfo.prepareConnectionsToSendOverLevel(hop.getLevel());

        if (connectionBuffer == null) {
            System.out
                .println("ConnectionPropagation:propagateConnBufferToHop-> "
                    + " No connections of interest"
                    + " need to be sent over as a connectionBufferStream");
            return;
        }

        byte[] connectionBufferStream = new byte[connectionBuffer.length + 1];
        connectionBufferStream[0] = Protocol.CONNECTION_BUFFER;
        System.arraycopy(connectionBuffer, 0, connectionBufferStream, 1,
                         connectionBuffer.length);
        protocolHandler.sendToNode(hop, connectionBufferStream);
        System.out.println("ConnectionPropagation:propagateConnBufferToHop-> "
            + " Propagation to " + hop + " complete");
    }


    public static void main(String[] args) {
        System.out.println("ConnectionPropagationProtocol");
    }
}
